iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
Rust

Bevy Rogue-lite 勇者冒險篇 × Rust 遊戲開發筆記系列 第 7

如何進出不同空間?

  • 分享至 

  • xImage
  •  

上一篇把隨機產生房間搞定後,角色雖然能在不同類型的房間出現,但是這個邏輯不太正確。應該會是角色在房間外的位置,然後需要經由門的出入才是正確的互動過程。

所以這一篇主要做的是,當隨機產生房間時,角色比較在外面,而且房間的門必須是可開關,如果是關起來的狀態,就不能進出,必須要開啟的門才能進出。

預計流程

這次處理的是一個「跨系統耦合」的問題。門的開關、玩家的移動、房間的傳送都屬於不同的系統,任何一個判斷錯誤就會讓流程結束。

所以我預想需求會拆成四個階段:

  • 互動輸入判斷:空白鍵的點擊既要能揮劍,也要在門邊開門。要先檢查角色附近是否有開門距離內的門,有就優先送出 DoorInteractionEvent,否則就觸發攻擊事件。
  • 移動向量與備援:房間切換依賴 InputVector 判斷方向,但角色按下空白鍵時輸入系統會把向量清成 Vec2::ZERO,造成碰撞檢查失效。所以我在系統中加入 Velocity 備援,必要時用速度來推估目前移動方向並重新正規化。
  • 傳送距離的正規化:計算使用 ROOM_TILE_SIZE * PLAYER_SCALE * 1.5,跟著瓷磚大小變動,門內外緩衝距離就會自動調整。
  • 入口空間與出生點:房間外需要一整塊可活動的室外地板,玩家的角色也應該在這裡產生。室外區域必須跟著房間寬度調整,並把門口座標記錄成資源,可以讓角色與後續系統使用。

整體流程回事:按空白鍵 → 門就緒 → 朝門走到觸發範圍 → 在移動方向與門位置條件成立時進行傳送。

程式碼解析

空白鍵優先門互動的輸入系統

pub fn input_system(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    player_query: Query<&Transform, With<Player>>,
    door_query: Query<(&Door, &Transform), (With<RoomTile>, Without<Player>)>,
    mut door_events: EventWriter<DoorInteractionEvent>,
    mut attack_events: EventWriter<AttackInputEvent>,
) {
    if keyboard_input.just_pressed(KeyCode::Space) {
        let player_transform = match player_query.single() {
            Ok(transform) => transform,
            Err(_) => return,
        };

        let interaction_distance = ROOM_TILE_SIZE * PLAYER_SCALE * 10.0;
        let mut near_door = false;

        for (_door, door_transform) in &door_query {
            if player_transform
                .translation
                .distance(door_transform.translation)
                <= interaction_distance
            {
                near_door = true;
                break;
            }
        }

        if near_door {
            door_events.write(DoorInteractionEvent);
            info!("門交互事件已發送!");
        } else {
            attack_events.write(AttackInputEvent);
        }
    }
}

這段邏輯可以確保點擊空白鍵可以處理門,也不會讓攻擊系統沒作用。玩家就可以不需要記兩個按鍵,使用體驗上可以更直觀。

備援移動向量與動態傳送距離

pub fn room_transition_system(
    door_query: Query<(&Door, &Transform), Without<Player>>,
    mut player_query: Query<(&mut Transform, &Velocity, &InputVector), With<Player>>,
    mut transition_cooldown: ResMut<TransitionCooldown>,
    time: Res<Time>,
) {
    transition_cooldown.timer.tick(time.delta());
    if !transition_cooldown.timer.finished() {
        return;
    }

    let (mut player_transform, velocity, input_vector) = match player_query.single_mut() {
        Ok(result) => result,
        Err(_) => return,
    };

    let tile_size = ROOM_TILE_SIZE * PLAYER_SCALE;
    let trigger_distance = tile_size * 3.0;
    let teleport_offset = tile_size * 1.5;

    let mut movement = input_vector.0;
    if movement.length_squared() <= f32::EPSILON {
        movement = Vec2::new(velocity.x, velocity.y);
        if movement.length_squared() > 0.0 {
            movement = movement.normalize();
        }
    }

    for (door, door_transform) in &door_query {
        let door_pos = door_transform.translation.truncate();
        let player_pos = player_transform.translation.truncate();

        if door.is_open && door_pos.distance(player_pos) < trigger_distance {
            let door_to_player = player_pos - door_pos;
            let moving_up = movement.y > 0.1;
            let moving_down = movement.y < -0.1;

            if door_to_player.y < -20.0 && moving_up {
                let new_position = door_pos + Vec2::new(0.0, teleport_offset);
                player_transform.translation.x = new_position.x;
                player_transform.translation.y = new_position.y;
                transition_cooldown.timer.reset();
                info!("✅ 玩家進入房間!從 {:?} 傳送到 {:?}", player_pos, new_position);
            } else if door_to_player.y > 20.0 && moving_down {
                let new_position = door_pos + Vec2::new(0.0, -teleport_offset);
                player_transform.translation.x = new_position.x;
                player_transform.translation.y = new_position.y;
                transition_cooldown.timer.reset();
                info!("✅ 玩家離開房間!從 {:?} 傳送到 {:?}", player_pos, new_position);
            }
        }
    }
}

這裡特別講一下兩個地方:

  1. Movement fallback:當 InputVector 因為按下互動鍵而歸零時,會立刻改用速度算方向,避免系統誤判玩家沒有在移動。
  2. Scale-aware offsetteleport_offset 按照地板大小計算,未來如果調整 PLAYER_SCALE 的時侯也不用再去動這段程式。

房間外入口區域與出生點

if should_generate_door {
    if let Some(door_pos) = door_world_position {
        let outdoor_depth_tiles = 5;
        let outdoor_extra_width = 4;
        let spawn_offset_tiles = 2;

        let half_width = (width as i32 + outdoor_extra_width) / 2;

        for depth in 1..=outdoor_depth_tiles {
            let exterior_y = door_pos.y - tile_size * depth as f32;

            for offset in -half_width..=half_width {
                let exterior_x = door_pos.x + offset as f32 * tile_size;

                commands.spawn((
                    Sprite::from_image(room_assets.floor_outdoor.clone()),
                    Transform::from_translation(Vec3::new(exterior_x, exterior_y, Z_LAYER_GRID))
                        .with_scale(Vec3::splat(PLAYER_SCALE)),
                    RoomTile {
                        tile_type: RoomTileType::FloorOutdoor,
                    },
                ));
            }
        }

        let spawn_y = door_pos.y - tile_size * spawn_offset_tiles as f32;
        commands.insert_resource(EntranceLocation::new(Vec3::new(door_pos.x, spawn_y, 10.0)));
    }
}

pub fn spawn_player(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    entrance_location: Option<Res<EntranceLocation>>,
) {
    let spawn_position = entrance_location
        .map(|location| location.position)
        .unwrap_or_else(|| Vec3::new(0.0, -ROOM_TILE_SIZE * PLAYER_SCALE * 3.0, 10.0));

    commands
        .spawn((
            Player,
            Sprite::from_image(asset_server.load("characters/knight_lv1.png")),
            Transform::from_translation(spawn_position)
                .with_scale(Vec3::splat(PLAYER_SCALE)),
            /* ... */
        ));
    // 省略武器子節點邏輯
}

EntranceLocation 紀錄門外的座標,PostStartup 階段的 spawn_player 從中取值,確保房間建立完畢後才產生角色。室外地板則根據房間寬度向左右延伸,比門寬多出兩格視覺緩衝,深度則向下鋪五格,形成一個暫時的戶外落腳區。

結果展示

實際測試流程:

  • 在門外按下空白鍵就可以開門。
  • 按住方向鍵朝門走,進入與離開都能穩定傳送到不同空間。
  • 碰撞系統一樣會阻擋關閉的門,開門後才能進入。
  • 角色產生在門外的區域,外面地面使用不同地板圖片,在視覺上可以區分室內外空間。

進出門demo

小結

目前的系統還有蠻多需要加強的,像是在進出空間時,會有一種跳躍的感覺,或許可以考慮加上一個過場的畫面來解決這個問題。不過以功能面來說算是達到需求了,後續進入打磨階段再來決定怎麼做比較好。

今天的程式碼分享在 repo


上一篇
隨機產生地圖
系列文
Bevy Rogue-lite 勇者冒險篇 × Rust 遊戲開發筆記7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言